<?php

/**
 * 登陆缓存功能
 * 整体依赖 think\Cache 的 redis 驱动方式
 * 
 * 储存结构:
 * 		1. 数据记录： 每个实例一条，唯一的记录每一条模型的数据实例
 * 			{前缀}{表名}_pk:{主键值} => {模型实例}
 * 		2. token记录： 每个实例多条，记录实例的主键值
 * 			{前缀}{表名}_token:{主键值}:{token值} => {模型主键值}
 * 
 * 流程:
 * 		1. 对模型实例调用 updateLoginCache() 创建数据记录
 * 		2. 对模型实例调用 getLoginToken() 获得一个token，此方法也可以强制产生新的token，此方法会自动维护 token 的 Set 清单
 * 		3. 通过静态方法 getLoginData($token) 查出模型实例数据，每次调用此方法，会自动更新该token的有效期
 * 		4. 清除token，可以对模型实例调用 clearLoginToken() ，如需删除所有模型的token，可调用静态方法 clearLoginCache(true)
 * 
 * 用法说明:
 * 		1. 模型use本类，在模型类中 use LoginCache，类似软删除
 * 		2. 如需修改配置，可在模型的 __construct($data = []) 中可调用 $this->setLoginCacheOption($option) 配置本功能，记得先 parent::__construct($data);
 * 		3. 可在模型的 onAfterWrite() 事件中调用 $admin->updateLoginCache(true); 方法来自动更新缓存的这条数据
 * 
 * 示例:
	// 单点登录
	$admin = Admin->find(1);
	$unionToken = $admin->clearLoginToken()->getLoginToken(true); // 获得单点唯一token

	// 多点登陆
	$admin = Admin->find(1);
	$token = $admin->getLoginToken(); // 获得可用的token

	// 根据token获得数据
	$token = 'xxxxx';
	$admin = Admin::getLoginData($token, true); //刷新该token的过期时间
	$admin = Admin::getLoginData($token, false); //不刷新过期时间
	if(is_null($admin)){
		// token没能找到对应模型数据
	}
 */


declare(strict_types=1);

namespace app\model\common;

use think\facade\Cache;

/**
 * @deprecated 本类可以废弃，全面升级为 TokenTrait
 */
trait LoginCacheTrait
{

	/**
	 * @var array 登陆缓存配置
	 */
	protected $login_cache_option = [
		// 过期时长 秒，0 表示不限制
		'expire_time' 	=> 7200,
		// 缓存前缀
		'prefix' 		=> 'login_',
		// 数据分隔符
		'separator'		 => ':',
	];

	/**
	 * 获取登陆缓存配置
	 * @param string|null $key 配置名（不传入获取所有）
	 */
	public function getLoginCacheOption($key = null, $default = null)
	{
		if (is_null($key)) return $this->login_cache_option;
		else return $this->login_cache_option[$key] ?? $default;
	}

	/**
	 * 设置登陆缓存配置
	 * 可以只配置一部分，内部采用数组合并，不会影响默认配置
	 * @param array $option 配置项，参考 $login_cache_option
	 */
	public function setLoginCacheOption(array $option)
	{
		$this->login_cache_option = array_merge($this->login_cache_option, $option);
		return $this;
	}

	/**
	 * 更新缓存的模型数据
	 * 默认只检查是否有缓存数据，如果没有就创建，有则直接返回
	 * @param boolean $force_update 是否强制更新模型数据到缓存中
	 * @param mixed $data 可以设置要储存的值，比如追加一些关联信息
	 */
	public function updateLoginCache($force_update = false, $data = null)
	{
		$data_key = $this->loginDataKey($this['id']);

		if ($force_update || !Cache::has($data_key)) Cache::set($data_key, is_null($data) ? $this : $data);
		return Cache::get($data_key);
	}

	/**
	 * 获得一个token
	 * 如果已存在就返回可用的
	 * @param boolean $create 是否强制创建新的token（不会影响其他有效token）
	 */
	public function getLoginToken($create = false): string
	{
		// 确保数据实例存在
		$this->updateLoginCache();

		$pk = $this[$this->getPk()];

		$token = false;
		if (!$create) {
			// 查询token
			$cursor = null;
			// 必须设置，扫描不到自动迭代尝试下一个，否则limit->1可能失败
			Cache::handler()->setOption(\Redis::OPT_SCAN, \Redis::SCAN_RETRY);
			$find_tokens = Cache::handler()->scan($cursor, Cache::getCacheKey($this->loginTokenKey($pk, '*')), 1);
			if ($find_tokens && count($find_tokens) > 0) {
				$separator = $this->getLoginCacheOption('separator', ':');
				$split = explode($separator, $find_tokens[0]);
				$token = $split[2];
			}
		}

		if (!$token) {
			$expire_time = $this->getLoginCacheOption('expire_time');
			$token = $this->loginMakeToken();
			Cache::set($this->loginTokenKey($pk, $token), $pk, $expire_time);
		}

		return $token;
	}

	/**
	 * 清除本模型实例的所有token缓存
	 * @param boolean $clear_data 是否同时清空模型数据
	 * @param string|null $pk 是否指定清除某个主键模型的数据（通常不使用）
	 * @return self
	 */
	public function clearLoginToken($clear_data = false, $pk = null)
	{
		if ($pk == null) $pk = $this[$this->getPk()];

		// 删除token清单
		$cursor = null;
		Cache::handler()->setOption(\Redis::OPT_SCAN, \Redis::SCAN_RETRY);
		$find_tokens = Cache::handler()->scan($cursor, Cache::getCacheKey($this->loginTokenKey($pk, '*')));
		Cache::handler()->del($find_tokens);

		// 删除数据
		if ($clear_data) {
			$cursor = null;
			Cache::handler()->setOption(\Redis::OPT_SCAN, \Redis::SCAN_RETRY);
			$find_tokens = Cache::handler()->scan($cursor, Cache::getCacheKey($this->loginDataKey($pk)));
			Cache::handler()->del($find_tokens);
		}
		return $this;
	}


	/**
	 * 通过token获取数据
	 * @param string $token token字符串
	 * @param boolean $update_expire 是否更新过期时间
	 * @return $this|null 若存在返回缓存的模型，不存在返回null
	 */
	public static function getLoginData(string $token, $update_expire = true)
	{
		$self = new self;

		$cursor = null;
		Cache::handler()->setOption(\Redis::OPT_SCAN, \Redis::SCAN_RETRY);
		$find_tokens = Cache::handler()->scan($cursor, Cache::getCacheKey($self->loginTokenKey('*', $token)), 1);

		$pk = null;
		if ($find_tokens && count($find_tokens)) $pk = Cache::handler()->get($find_tokens[0]);

		$data = $pk ? Cache::get($self->loginDataKey($pk)) : null;

		// 如果数据已不存在，清除所有token
		if ($pk && is_null($data)) $self->clearLoginToken(true, $pk);

		// 更新缓存时间
		if ($data && $update_expire) Cache::set($self->loginTokenKey($pk, $token), $pk, $self->getLoginCacheOption('expire_time'));

		return $data;
	}

	/**
	 * 清空所有token和缓存数据
	 * @param boolean $clear_data 是否同时清空缓存的数据
	 */
	public static function clearLoginCache($clear_data = false)
	{
		$self = new self;

		// 删除token清单
		$cursor = null;
		Cache::handler()->setOption(\Redis::OPT_SCAN, \Redis::SCAN_RETRY);
		$find_tokens = Cache::handler()->scan($cursor, Cache::getCacheKey($self->loginTokenKey(null)));
		Cache::handler()->del($find_tokens);

		// 删除数据
		if ($clear_data) {
			$cursor = null;
			Cache::handler()->setOption(\Redis::OPT_SCAN, \Redis::SCAN_RETRY);
			$find_tokens = Cache::handler()->scan($cursor, Cache::getCacheKey($self->loginDataKey('*')));
			Cache::handler()->del($find_tokens);
		}
	}

	// ==== 私有方法 ====

	/**
	 * 获取模型唯一识别名
	 */
	protected function loginGetModelName(): string
	{
		return $this->getTable();
	}

	/**
	 * 创建随机token
	 * @param integer $rand_len
	 * @return string
	 */
	protected function loginMakeToken(): string
	{
		return md5(uniqid(md5((string)microtime(true)), true));
	}

	/**
	 * 获取数据储存的key名
	 */
	protected function loginDataKey($pk = ''): string
	{
		return $this->getLoginCacheOption('prefix', '') . $this->loginGetModelName() . '_pk' . $this->getLoginCacheOption('separator', ':') . $pk;
	}

	/**
	 * 获得token储存的key名
	 * @param string|null $pk 模型主键，如果是null则返回前缀加 * ，用于查找多个
	 */
	protected function loginTokenKey($pk = '', $token = ''): string
	{
		$separator = $this->getLoginCacheOption('separator', ':');

		$pre = $this->getLoginCacheOption('prefix') . $this->loginGetModelName() . '_token';
		return is_null($pk)
			? $pre . ':*'
			: $pre . $separator . $pk . $separator . $token;
	}
}
